<?php

// ------------------------------------------------------------------------
// |@Author       : Jarmin <edshop@qq.com>
// |@----------------------------------------------------------------------
// |@Date         : 2023-06-05 15:17:15
// |@----------------------------------------------------------------------
// |@LastEditTime : 2025-04-21 14:41:12
// |@----------------------------------------------------------------------
// |@LastEditors  : Jarmin <jarmin@ladmin.cn>
// |@----------------------------------------------------------------------
// |@Description  : 随机数码管理扩展（提供加密安全的数据处理功能）
// |@----------------------------------------------------------------------
// |@FilePath     : CodeExtend.php
// |@----------------------------------------------------------------------
// |@Copyright (c) 2023 http://www.ladmin.cn   All rights reserved.
// ------------------------------------------------------------------------
declare (strict_types=1);

namespace think\admin\extend;

use think\exception\HttpResponseException;
use think\Response;

/**
 * 随机数码管理扩展
 *
 * 提供企业级加密安全的数据处理功能，包括：
 * - 符合RFC 4122标准的UUIDv4生成
 * - AES-256-CBC数据加解密
 * - 抗暴力破解的密码哈希
 * - 安全随机字符串生成
 * - 数据压缩/解压
 * @package think\admin\extend
 */
class CodeExtend
{
    /**
     * 生成符合UUIDv4标准的唯一标识
     * @return string 格式：8-4-4-4-12的十六进制字符串（大写）
     */
    public static function uuid(): string
    {
        // 生成16字节密码学安全随机数（满足UUIDv4的熵要求）
        $bytes = random_bytes(16);
        // 设置版本位：第7字节的高4位设为0100（0x40），标记为UUIDv4
        $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40);
        // 设置变体位：第9字节的高2位设为10（0x80），符合RFC 4122规范
        $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80);
        // 将二进制转换为十六进制并按UUID格式分割
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
    }

    /**
     * 生成密码学安全随机字符串
     * @param int $size 字符串长度（4-1024）
     * @param int $type 字符类型（1=纯数字，2=纯小写字母，3=数字+字母）
     * @param string $prefix 固定前缀（不计入长度）
     * @return string 生成的随机字符串
     * @throws HttpResponseException
     */
    public static function random(int $size = 10, int $type = 1, string $prefix = ''): string
    {
        // 强制最小长度防止生成不安全短字符串
        if ($size < 4) {
            self::throwJsonError(400, [
                'code'    => 4002,
                'message' => lang('service.codeextend_invalid_min_length'),
                'data'    => ['min_length' => 4]
            ]);
        }

        // 验证字符类型合法性（1=数字，2=小写字母，3=混合）
        if (!in_array($type, [1, 2, 3])) {
            self::throwJsonError(400, [
                'code'    => 4001,
                'message' => lang('service.codeextend_invalid_char_type', ['allowed_types' => [1, 2, 3]]),
                'data'    => ['allowed_types' => [1, 2, 3]]
            ]);
        }

        // 根据类型选择字符集
        $chars = match ($type) {
            1 => '0123456789',       // 适用于短信验证码等纯数字场景
            2 => 'abcdefghijklmnopqrstuvwxyz', // 适用于临时密码生成
            3 => '0123456789abcdefghijklmnopqrstuvwxyz', // 通用场景
        };

        // 生成每个字符时使用密码学安全随机数
        $maxIndex = strlen($chars) - 1;
        $code = $prefix;
        for ($i = 0; $i < $size; $i++) {
            // 使用random_int而非mt_rand以保证安全性
            $code .= $chars[random_int(0, $maxIndex)];
        }
        return $code;
    }

    /**
     * 生成带日期前缀的编码
     * @param int $size 总长度（最小14位）
     * @param string $prefix 自定义前缀（默认空）
     * @return string 格式：YYYYMMDDHHmmss + 随机数字
     */
    public static function uniqidDate(int $size = 16, string $prefix = ''): string
    {
        // 确保最小长度包含完整日期（14位年月日时分秒）
        if ($size < 14) {
            $size = 14;
        }
        // 生成基础编码（日期+前缀）
        $code = $prefix . date('YmdHis');
        // 填充随机数字直到达到指定长度
        while (strlen($code) < $size) {
            $code .= random_int(0, 9); // 使用安全随机数生成器
        }
        return $code;
    }

    /**
     * AES-256-CBC 数据加密
     * @param mixed $data 可序列化的数据
     * @param string $skey 加密密钥（必须32字节）
     * @return string Base64Url编码的加密结果
     * @throws HttpResponseException
     */
    public static function encrypt($data, string $skey): string
    {
        // 强制256位密钥长度校验（32字节）
        if (strlen($skey) !== 32) {
            self::throwJsonError(500, [
                'code'    => 5001,
                'message' => lang('service.codeextend_aes256_key_required', ['received_length' => strlen($skey)]),
                'data'    => ['received_length' => strlen($skey)]
            ]);
        }

        // 生成随机初始化向量（每次加密不同，防止重放攻击）
        $iv = random_bytes(16);
        // 序列化数据以适应复杂数据结构
        $value = openssl_encrypt(serialize($data), 'AES-256-CBC', $skey, 0, $iv);

        // 处理底层加密失败（如系统缺少加密模块）
        if ($value === false) {
            self::throwJsonError(500, [
                'code'    => 5002,
                'message' => lang('service.codeextend_encrypt_failed', ['openssl_error' => openssl_error_string()]),
                'data'    => ['openssl_error' => openssl_error_string()]
            ]);
        }

        // 组合加密要素并进行安全编码
        return static::enSafe64(json_encode([
            'iv' => base64_encode($iv),  // 存储Base64编码的IV
            'value' => $value            // 加密后的二进制数据
        ]));
    }

    /**
     * AES-256-CBC 数据解密
     * @param string $data Base64Url编码的加密数据
     * @param string $skey 加密密钥
     * @return mixed 原始数据
     * @throws HttpResponseException
     */
    public static function decrypt(string $data, string $skey)
    {
        // 解码并验证数据结构完整性
        $json = static::deSafe64($data);
        $attr = json_decode($json, true);

        // 验证JSON格式和必要字段存在性
        if (json_last_error() !== JSON_ERROR_NONE || !isset($attr['iv'], $attr['value'])) {
            self::throwJsonError(422, [
                'code'    => 4002,
                'message' => lang('service.codeextend_invalid_encrypt_data', ['input_sample' => substr($data, 0, 32)]),
                'data'    => ['input_sample' => substr($data, 0, 32)]
            ]);
        }

        // 还原初始化向量（IV）并进行解密操作
        $iv = base64_decode($attr['iv']);
        $decrypted = openssl_decrypt($attr['value'], 'AES-256-CBC', $skey, 0, $iv);

        // 处理解密失败（可能密钥错误或数据篡改）
        if ($decrypted === false) {
            self::throwJsonError(500, [
                'code'    => 5003,
                'message' => lang('service.codeextend_decrypt_failed', ['openssl_error' => openssl_error_string()]),
                'data'    => ['openssl_error' => openssl_error_string()]
            ]);
        }

        // 反序列化原始数据
        return unserialize($decrypted);
    }

    /**
     * 使用Argon2id算法加密密码
     * @param string $password 明文密码
     * @param string $salt 加密盐值
     * @return string 加密后的哈希值
     */
    public static function encryptPassword(string $password, string $salt): string
    {
        // 使用固定的盐值进行预处理
        $preparedPassword = hash('sha256', $password . $salt);
        // 使用 password_hash 生成最终的哈希值
        return password_hash($preparedPassword, PASSWORD_BCRYPT, ['cost' => 12]);
    }

    /**
     * 验证密码哈希
     * @param string $password 待验证密码
     * @param string $salt 注册时使用的盐值
     * @param string $hash 存储的哈希值
     * @return bool 验证结果
     */
    public static function verifyPassword(string $password, string $salt, string $hash): bool
    {
        // 使用相同的预处理方式
        $preparedPassword = hash('sha256', $password . $salt);
        // 验证密码
        return password_verify($preparedPassword, $hash);
    }

    /**
     * 生成加密安全码
     * @return string 40字符的十六进制字符串（160位熵）
     */
    public static function generateCode(int $length = 20): string
    {
        // 生成20字节随机数（160位安全强度）
        return bin2hex(random_bytes(($length / 2)));
    }

    /**
     * Base64Url安全编码
     * @param string $text 原始字符串
     * @return string 编码后的字符串
     */
    public static function enSafe64(string $text): string
    {
        // URL安全编码：替换+/为-_，去除填充等号
        return rtrim(strtr(base64_encode($text), '+/', '-_'), '=');
    }

    /**
     * Base64Url安全解码
     * @param string $text 编码后的字符串
     * @return string 解码后的原始字符串
     */
    public static function deSafe64(string $text): string
    {
        // 恢复标准base64字符集
        $text = strtr($text, '-_', '+/');
        // 计算填充长度（base64需要4字节对齐）
        $padLength = ((4 - (strlen($text) % 4)) % 4);
        // 添加填充后解码
        return base64_decode($text . str_repeat('=', $padLength));
    }

    /**
     * 安全压缩数据（带错误处理）
     * @param mixed $data 可序列化数据
     * @return string 压缩后的Base64Url字符串
     * @throws HttpResponseException
     */
    public static function enzip($data): string
    {
        // 使用最大压缩级别（9）压缩序列化数据
        $compressed = gzcompress(serialize($data), 9);

        // 处理压缩失败（如数据类型不支持）
        if ($compressed === false) {
            self::throwJsonError(500, [
                'code'    => 5004,
                'message' => lang('service.codeextend_compress_failed', ['data_type' => gettype($data)]),
                'data'    => ['data_type' => gettype($data)]
            ]);
        }

        // 返回安全编码后的字符串
        return static::enSafe64($compressed);
    }

    /**
     * 安全解压数据（带验证）
     * @param string $string 压缩后的字符串
     * @return mixed 原始数据
     * @throws HttpResponseException
     */
    public static function dezip(string $string)
    {
        // 解压缩并验证数据完整性
        $data = gzuncompress(static::deSafe64($string));

        // 处理解压失败（如数据损坏）
        if ($data === false) {
            self::throwJsonError(500, [
                'code'    => 5005,
                'message' => lang('service.codeextend_decompress_failed', ['input_length' => strlen($string)]),
                'data'    => ['input_length' => strlen($string)]
            ]);
        }

        // 反序列化原始数据
        return unserialize($data);
    }

    /**
     * 抛出 JSON 格式响应异常
     * @param int $status  HTTP 状态码
     * @param array $error 错误内容数组
     */
    private static function throwJsonError(int $status, array $error)
    {
        // 创建标准JSON响应（保持错误格式统一）
        $response = Response::create($error, 'json', $status)
            ->header(['Content-Type' => 'application/json']);

        // 抛出可被框架全局捕获的异常
        throw new HttpResponseException($response);
    }
}
